/**
 * Date instance methods
 *
 * @author Ken Snyder (ken d snyder at gmail dot com)
 * @date 2008-04-21
 * @version 2.0 (http://kendsnyder.com/sandbox/date/)
 * @license Creative Commons Attribution License 3.0 (http://creativecommons.org/licenses/by/3.0/)
 */
// begin by creating a scope for utility variables
(function() {
  //
  // pre-calculate the number of milliseconds in a day
  //
  var day = 24 * 60 * 60 * 1000;
	//
	// function to add leading zeros
	//
	var zeroPad = function(number, digits) {
		number = String(number);
		while (number.length < digits) number = '0' + number;
		return number;
	};
  //
  // set up integers and functions for adding to a date or subtracting two dates
  //
  var multipliers = {
    millisecond: 1,
    second: 1000,
    minute: 60 * 1000,
    hour: 60 * 60 * 1000,
    day: day,
    week: 7 * day,
    month: {
      // add a number of months
      add: function(d, number) {
        // add any years needed (increments of 12)
        multipliers.year.add(d, Math[number > 0 ? 'floor' : 'ceil'](number / 12));
        // ensure that we properly wrap betwen December and January
        var prevMonth = d.getMonth() + (number % 12);
        prevMonth = prevMonth == 12 ? 0 : (prevMonth == -1 ? 11 : prevMonth);
        d.setMonth(prevMonth);
      },
      // get the number of months between two Date objects (decimal to the nearest day)
      diff: function(d1, d2) {
        // get the number of years
        var diffYears = d1.getFullYear() - d2.getFullYear();
        // get the number of remaining months
        var diffMonths = d1.getMonth() - d2.getMonth() + (diffYears * 12);
        // get the number of remaining days
        var diffDays = d1.getDate() - d2.getDate();
        // return the month difference with the days difference as a decimal
        return diffMonths + (diffDays / 30);
      }
    },
    year: {
      // add a number of years
      add: function(d, number) {
        d.setYear(d.getFullYear() + Math[number > 0 ? 'floor' : 'ceil'](number));
      },
      // get the number of years between two Date objects (decimal to the nearest day)
      diff: function(d1, d2) {
        return multipliers.month.diff(d1, d2) / 12;
      }
    }    
  };
  //
  // alias each multiplier with an 's' to allow 'year' and 'years' for example
  //
  for (var unit in multipliers) {
		if (unit.substring(unit.length - 1) != 's') { // IE will iterate newly added properties :|
	    multipliers[unit + 's'] = multipliers[unit];
		}
  }
	//
	// take a date instance and a format code and return the formatted value
	//
	var format = function(d, code) {
		if (Date.prototype.strftime.formatShortcuts[code]) {
			// process any shortcuts recursively
			return d.strftime(Date.prototype.strftime.formatShortcuts[code]);
		} else {
			// get the format code function and toPaddedString() argument
			var getter = (Date.prototype.strftime.formatCodes[code] || '').split('.');
			var nbr = d['get' + getter[0]] ? d['get' + getter[0]]() : '';
			// run toPaddedString() if specified
			if (getter[1]) nbr = zeroPad(nbr, getter[1]);
			// prepend the leading character
			return nbr;
		}	
	};
  //
  // Add methods to Date instances
  //
  var instanceMethods = {
    //
    // Return a date one day ahead (or any other unit)
    //
    // @param string unit
    // units: year | month | day | week | hour | minute | second | millisecond
    // @return object Date
    //
    succ: function(unit) {
      return this.clone().add(1, unit);
    },
    //
    // Add an arbitrary amount to the currently stored date
    //
    // @param integer/float number    
    // @param string unit
    // @return object Date (chainable)    
    //
    add: function(number, unit) {
      var factor = multipliers[unit] || multipliers.day;
      if (typeof factor == 'number') {
        this.setTime(this.getTime() + (factor * number));
      } else {
        factor.add(this, number);
      }
      return this;
    },
    //
    // Find the difference between the current and another date
    //
    // @param string/object dateObj
    // @param string unit
    // @param boolean allowDecimal
    // @return integer/float
    //
    diff: function(dateObj, unit, allowDecimal) {
      // ensure we have a Date object
      dateObj = Date.create(dateObj);
      if (dateObj === null) return null;
      // get the multiplying factor integer or factor function
      var factor = multipliers[unit] || multipliers.day;
      if (typeof factor == 'number') {
        // multiply
        var unitDiff = (this.getTime() - dateObj.getTime()) / factor;
      } else {
        // run function
        var unitDiff = factor.diff(this, dateObj);
      }
      // if decimals are not allowed, round toward zero
      return (allowDecimal ? unitDiff : Math[unitDiff > 0 ? 'floor' : 'ceil'](unitDiff));      
    },
    //
    // Convert a date to a string using traditional strftime format codes
    //
    // @param string formatStr
    // @return string
    //
    strftime: function(formatStr) {
      // default the format string to year-month-day
      var source = formatStr || '%Y-%m-%d', result = '', match;
      // replace each format code
			while (source.length > 0) {
				if (match = source.match(Date.prototype.strftime.formatCodes.matcher)) {
	        result += source.slice(0, match.index);
	        result += (match[1] || '') + format(this, match[2]);
	        source = source.slice(match.index + match[0].length);
	      } else {
	        result += source, source = '';
	      }
	    }
			return result;
		},
    //
    // Return a proper two-digit year integer
    //
    // @return integer
    //
    getShortYear: function() {
      return this.getYear() % 100;
    },
    //
    // Get the number of the current month, 1-12
    //
    // @return integer
    //
    getMonthNumber: function() {
      return this.getMonth() + 1;
    },
    //
    // Get the name of the current month
    //
    // @return string
    //
    getMonthName: function() {
      return Date.MONTHNAMES[this.getMonth()];
    },
    //
    // Get the abbreviated name of the current month
    //
    // @return string
    //
    getAbbrMonthName: function() {
      return Date.ABBR_MONTHNAMES[this.getMonth()];
    },
    //
    // Get the name of the current week day
    //
    // @return string
    //    
    getDayName: function() {
      return Date.DAYNAMES[this.getDay()];
    },
    //
    // Get the abbreviated name of the current week day
    //
    // @return string
    //    
    getAbbrDayName: function() {
      return Date.ABBR_DAYNAMES[this.getDay()];
    },
    //
    // Get the ordinal string associated with the day of the month (i.e. st, nd, rd, th)
    //
    // @return string
    //    
    getDayOrdinal: function() {
      return Date.ORDINALNAMES[this.getDate() % 10];
    },
    //
    // Get the current hour on a 12-hour scheme
    //
    // @return integer
    //
    getHours12: function() {
      var hours = this.getHours();
      return hours > 12 ? hours - 12 : (hours == 0 ? 12 : hours);
    },
    //
    // Get the AM or PM for the current time
    //
    // @return string
    //
    getAmPm: function() {
      return this.getHours() >= 12 ? 'PM' : 'AM';
    },
    //
    // Get the current date as a Unix timestamp
    //
    // @return integer
    //
    getUnix: function() {
      return Math.round(this.getTime() / 1000, 0);
    },
    //
    // Get the GMT offset in hours and minutes (e.g. +06:30)
    //
    // @return string
    //
    getGmtOffset: function() {
      // divide the minutes offset by 60
      var hours = this.getTimezoneOffset() / 60;
      // decide if we are ahead of or behind GMT
      var prefix = hours < 0 ? '+' : '-';
      // remove the negative sign if any
      hours = Math.abs(hours);
      // add the +/- to the padded number of hours to : to the padded minutes
      return prefix + zeroPad(Math.floor(hours), 2) + ':' + zeroPad((hours % 1) * 60, 2);
    },
    //
    // Get the browser-reported name for the current timezone (e.g. MDT, Mountain Daylight Time)
    //
    // @return string
    //
    getTimezoneName: function() {
      var match = /(?:\((.+)\)$| ([A-Z]{3}) )/.exec(this.toString());
			return match[1] || match[2] || 'GMT' + this.getGmtOffset();
    },
    //
    // Convert the current date to an 8-digit integer (%Y%m%d)
    //
    // @return int
    //
    toYmdInt: function() {
      return (this.getFullYear() * 10000) + (this.getMonthNumber() * 100) + this.getDate();
    },	
    //
    // Create a copy of a date object
    //
    // @return object
    //		
		clone: function() {
			return new Date(this.getTime());
		}
  };
	for (var name in instanceMethods) Date.prototype[name] = instanceMethods[name];
  //
  // Add static methods to the date object
  //
  var staticMethods = {
    //
    // The heart of the date functionality: returns a date object if given a convertable value
    //
    // @param string/object/integer date
    // @return object Date
    //
    create: function(date) {
      // If the passed value is already a date object, return it
      if (date instanceof Date) return date;
      // If the passed value is an integer, interpret it as a unix timestamp
      if (typeof date == 'number') return new Date(date * 1000);
      // If the passed value is a string, attempt to parse it using Date.parse()
			var parsable = String(date).replace(/^\s*(.+)\s*$/, '$1'), i = 0, length = Date.create.patterns.length, pattern;
			var current = parsable;
			while (i < length) {
				ms = Date.parse(current);
				if (!isNaN(ms)) return new Date(ms);
				pattern = Date.create.patterns[i];
				if (typeof pattern == 'function') {
					obj = pattern(current);
					if (obj instanceof Date) return obj;
				} else {
					current = parsable.replace(pattern[0], pattern[1]);
				}
				i++;
			}
			return NaN;
    },
    //
    // constants representing month names, day names, and ordinal names
    // (same names as Ruby Date constants)
    //
    MONTHNAMES      : 'January February March April May June July August September October November December'.split(' '),
    ABBR_MONTHNAMES : 'Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec'.split(' '),
    DAYNAMES        : 'Sunday Monday Tuesday Wednesday Thursday Friday Saturday'.split(' '),
    ABBR_DAYNAMES   : 'Sun Mon Tue Wed Thu Fri Sat'.split(' '),
    ORDINALNAMES    : 'th st nd rd th th th th th th'.split(' '),
    //
    // Shortcut for full ISO-8601 date conversion
    //
    ISO: '%Y-%m-%dT%H:%M:%S.%N%G',
    //
    // Shortcut for SQL-type formatting
    //
    SQL: '%Y-%m-%d %H:%M:%S',
    //
    // Setter method for month, day, and ordinal names for i18n
    //
    // @param object newNames
    //
  	daysInMonth: function(year, month) {
  		if (month == 2)
  			return new Date(year, 1, 29).getDate() == 29 ? 29 : 28;
  		return [undefined,31,undefined,31,30,31,30,31,31,30,31,30,31][month];
  	}
  };
	for (var name in staticMethods) Date[name] = staticMethods[name];
 	//
  // format codes for strftime
  //
  // each code must be an array where the first member is the name of a Date.prototype function
  // and optionally a second member indicating the number to pass to Number#toPaddedString()
  //
	Date.prototype.strftime.formatCodes = {
    //
    // 2-part regex matcher for format codes
    //
    // first match must be the character before the code (to account for escaping)
    // second match must be the format code character(s)
    //
    matcher: /()%(#?(%|[a-z]))/i,
    // year
    Y: 'FullYear',
    y: 'ShortYear.2',
    // month
    m: 'MonthNumber.2',
 '#m': 'MonthNumber',
    B: 'MonthName',
    b: 'AbbrMonthName',
    // day
    d: 'Date.2',
 '#d': 'Date',
    e: 'Date',
    A: 'DayName',
    a: 'AbbrDayName',
    w: 'Day',
    o: 'DayOrdinal',
    // hours
    H: 'Hours.2',
 '#H': 'Hours',
    I: 'Hours12.2',
 '#I': 'Hours12',
    p: 'AmPm',
    // minutes
    M: 'Minutes.2',
 '#M': 'Minutes',
    // seconds
    S: 'Seconds.2',
 '#S': 'Seconds',
    s: 'Unix',
    // milliseconds
    N: 'Milliseconds.3',
 '#N': 'Milliseconds',
    // timezone
    O: 'TimezoneOffset',
    Z: 'TimezoneName',
    G: 'GmtOffset'  
  };
  //
  // shortcuts that will be translated into their longer version
  //
  // be sure that format shortcuts do not refer to themselves: this will cause an infinite loop
  //
  Date.prototype.strftime.formatShortcuts = {
    // date
    F: '%Y-%m-%d',
    // time
    T: '%H:%M:%S',
    X: '%H:%M:%S',
    // local format date
    x: '%m/%d/%y',
    D: '%m/%d/%y',
    // local format extended
 '#c': '%a %b %e %H:%M:%S %Y',
    // local format short
    v: '%e-%b-%Y',
    R: '%H:%M',
    r: '%I:%M:%S %p',
    // tab and newline
    t: '\t',
    n: '\n',
  '%': '%'
  };
  //
  // A list of conversion patterns (array arguments sent directly to gsub)
  // Add, remove or splice a patterns to customize date parsing ability
  //
	Date.create.patterns = [
    [/-/g, '/'], // US-style time with dashes => Parsable US-style time
		[/st|nd|rd|th/g, ''], // remove st, nd, rd and th    
    [/(3[01]|[0-2]\d)\s*\.\s*(1[0-2]|0\d)\s*\.\s*([1-9]\d{3})/, '$2/$1/$3'], // World time => Parsable US-style time
    [/([1-9]\d{3})\s*-\s*(1[0-2]|0\d)\s*-\s*(3[01]|[0-2]\d)/, '$2/$3/$1'], // ISO-style time => Parsable US-style time
		function(str) { // 12-hour time
			var match = str.match(/^(?:(.+)\s+)?([1-9]|1[012])(?:\s*\:\s*(\d\d))?(?:\s*\:\s*(\d\d))?\s*(am|pm)\s*$/i);
			//                      ^opt. date  ^hour         ^opt. minute       ^opt. second          ^am or pm
			if (match) {
				if (match[1]) {
					var d = Date.create(match[1]);
					if (isNaN(d)) return;
				} else {
					var d = new Date();
					d.setMilliseconds(0);
				}
				var hour = parseFloat(match[2]);
				hour = match[5].toLowerCase() == 'am' ? (hour == 12 ? 0 : hour) : (hour == 12 ? 12 : hour + 12);
				d.setHours(hour, parseFloat(match[3] || 0), parseFloat(match[4] || 0));
				return d;
			}
		}		
  ];  
})();
//
// Create a convenience method for creating dates from strings
//
var $D = Date.create;
